# pwr_cmdr.pl
#
# April 2010, James Redus   
#
# This script is released under the terms of the MIT License:
# --------------------------------------------------------------
## Permission is hereby granted, free of charge, to any person
## obtaining a copy of this software and associated documentation
## files (the "Software"), to deal in the Software without
## restriction, including without limitation the rights to use,
## copy, modify, merge, publish, distribute, sublicense, and/or sell
## copies of the Software, and to permit persons to whom the
## Software is furnished to do so, subject to the following
## conditions:
##
## The above copyright notice and this permission notice shall be
## included in all copies or substantial portions of the Software.
##
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
## OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
## HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
## WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
## OTHER DEALINGS IN THE SOFTWARE.
# --------------------------------------------------------------
# 4/11/2010,jeredus - Added project to Google code (http://code.google.com/p/djmaputils/)
# 
# This file contains functions that are used by other scripts.
# 
# NOTE: The help/coding comments below are incomplete (jer,4/11/2010).
#
# These (experimental) subs are intended to read Dynojet Power Commander III and V
# files (PC3 files have .DJM extension, PC5 files have .PVM extension).
#
# I've probably accounted for about 95% of the bytes in the PC3 files, and 
# maybe 75% of the bytes in the PC5 file (but I got the 'main stuff'
# from each file type). I would not be surprised
# to find that these funcs don't work on some PC3/5 files (especially PC5).
#
# To call the subs from another Perl script, do
# require 'pwr_cmdr.pl';
# and then pass the sub the filename, storing results in a hash var.
#
# %h=read_pvm_file(my_pc5_file.pvm,$verbose);
# %h=read_djm_file(my_pc3_file.djm,$verbose);
#
# Hashes returned for either map type contain :
# --------------------------------------------------
# h{fuel_map} => ARRAY in row-order containing fuel map values
# h{fuel_rpm_count} => Integer - number of RPMs in fuel map (e.g., 51 or 54 for FZ1).
# h{fuel_rpm_labels} => ARRAY containing fuel RPM labels (500,750,...)
# h{text} => String - the TEXT in the comments for the map file.
# h{map_file} => String - the map filename
# h{map_type} => String - either 'PC3' or 'PC5'
# 
# PC3 map hashes also contain this:
# --------------------------------------------------
# h{ign_map} => ARRAY in row-order containing IGN map values
# h{ign_rpm_count} => Integer - number of RPMs in IGN map
# h{ign_rpm_labels} => ARRAY containing IGN RPM labels (500,750,...)
# h{throttle_positions} => ARRAY containing throttle positions (0,2,,...100); 
# (these are well known tho, so not really needed)
# 
# PC5 map hashes also contain this:
# --------------------------------------------------
# h{afr_table} => ARRAY in row-order containing Air-Fuel ratios 
# h{trim_table} => ARRAY in row-order containing Trim table 
# h{unknown_table} => ARRAY (same size as other PC5 arrays); future IGN map??
#
# EXAMPLE (printing fuel map):
# ===============================
#     %h=read_djm_file($mapfile,0);
#
#
#     if ($h{map_type} eq "PC3") {
#        $tp_count=9;
#     }elsif($h{map_type} eq "PC5" ) {
#        $tp_count=10;
#     }else{
#         print "Unknown hash type - aborting!\n";
#         exit(1);
#     }
#     $rpm_count=$h{fuel_rpm_count};
#     @fuel_array=@{$h{fuel_map}};
#   
#     for ($rpm=0; $rpm<$rpm_count; $rpm++) {
#         print "${$h{fuel_rpm_labels}}[$rpm] : ";
#         for ($tp=0; $tp<$tp_count; $tp++) {
#             print "${$h{fuel_map}}[ $rpm*$tp_count + $tp ] ";
#         }
#         print "\n";
#     }
#
#
    
use File::Basename;

my $VERSION="1.0";

###########################################################
# Call this to read from file.
sub my_read
{
    my ($fh,$desired_bytes) = @_;
    my $actual_bytes,$buff;
    
    $actual_bytes=read $fh, $buff, $desired_bytes;

    if ($actual_bytes != $desired_bytes) {
        print "OOPS! Read Error - $desired_bytes/$actual_bytes ($!)!\n";
        print "Aborting...\n";
        exit(1);
    }

    return($buff);
}
###########################################################
#
# This func reads a PC3 (.djm) file. The useful stuff will
# be stored in a hash (%h) and returned.
#
#
sub read_djm_file
{
    my ($file,$verbose) = @_;
    my ($buff,%h,$text_len,$mapnum,$rpm_count,@rpm_labels,@throttle_positions,@arr,@arr2,$rpm,$tp,$val);
    
    print "Opening map file: $file\n"  if ($verbose);
    if (! open(INFILE, "$file") ) {
        print "Unable to read $file ($!) - skipping it.\n";
        return;
    }
    binmode(INFILE);

    %h=();
    @arr=();
    @arr2=();
    
    # 1st 12 bytes seem to be 'CA 00 00 00 98 00 00 00 02 00 09 00' for PC3.
    # 4th 32-bit integer is the length of the TEXT.
    $buff=my_read(*INFILE,16);
    $val=(unpack("VVVV",$buff))[0]; 

    # hex(CA) = 202
    if ( $val != 202 ) {
        print "Questionable value in bytes 1-4 ($val) - Aborting map!\n";
        close INFILE;
        return;
    }

    $h{map_type}="PC3";
    $h{map_file}=$file;
    $h{map_basename}=basename($file);
    $h{map_dirname}=dirname($file);

    $text_len=(unpack("VVVV",$buff))[3]; 
    $h{text_len}=$text_len;
    print "text_len=$text_len\n" if ($verbose);
    
    # After the text len is 8 bytes that seems to vary somewhat (no clue what its for).
    # Then there's a 32-bit integer (250 - rpm increment??). Don't see a use for it anyway.
    $buff=my_read(*INFILE,12);
    
    # Now we're at the start of TEXT.
    $h{text}=my_read(*INFILE,$text_len);

    # After the TEXT are 9 32-bit TPs (0,2,5,10,...,100)
    $buff=my_read(*INFILE,36);
    (@throttle_positions)=unpack('V*',$buff);
    @{$h{throttle_positions}}=@throttle_positions;
    
    print "throttle_positions=@throttle_positions\n" if ($verbose);


    # After TPs there appears to be 2 short ints. 1st looks like 
    # a map# (map#1=fuel, map#2=ignition, ?)
    # The 2nd value is the number of rows (RPMs) in the fuel map.
    $buff=my_read(*INFILE,4);
    ($mapnum,$rpm_count)=unpack("vv",$buff);
    $h{fuel_rpm_count}=$rpm_count;

    print "rpm_count=$rpm_count\n" if ($verbose);
    if ( ($rpm_count < 20) || ( $rpm_count > 80 ) ) {
        print "Questionable rpm_count ($rpm_count) - Aborting map!\n";
        close INFILE;
        return;
    } 

    # Next is 16 bytes that I have no idea about (skipping past it)
    $buff=my_read(*INFILE,16);
    
    # Next there are the N ($rpm_count) 4-byte RPM labels (500,750,...)
    # And then the comes the columns of fuel map data (N values for each TP).
    $buff=my_read(*INFILE,$rpm_count*4);
    (@rpm_labels)=unpack('V*',$buff);

    @{$h{fuel_rpm_labels}}=@rpm_labels;  # there can be a different set of rpms for ign.
    print "rpm_labels=@rpm_labels\n" if ($verbose);
    
    # The fuel map is arranged in the file in column-order (9 columns - 1 for each TP).
    # It's easier for me to keep it straight to go ahead and re-arrange it into row-order.
    #
    $buff=my_read(*INFILE,$rpm_count*4*9);   
    (@arr)=unpack('i*',$buff);  # 9 columns of data in one big 'ol array.

    # Re-order FUEL MAP (by-row rather than by-column):
    @arr2=();
    for ($rpm=0; $rpm<$rpm_count; $rpm++) {
        for ($tp=0; $tp<9; $tp++) {
            $arr2[$rpm*9 + $tp]=$arr[ $tp*$rpm_count + $rpm ];
        }
    }

    @{$h{pc3_fuel_map}}=@arr2;  
    @{$h{fuel_map}}=@arr2;      # common name for other scripts to work with

    if ($verbose) {
        for ($rpm=0; $rpm<$rpm_count; $rpm++) {
            print "Fuel map [$rpm_labels[$rpm]] : ";
            for ($tp=0; $tp<9; $tp++) {
                print "$arr2[$rpm*9 + $tp] ";
                print "\n" if ($tp == 8);
            }
        } 
    } 

    # Next is the IGNITION map.
    # It's about the same as the fuel map, starting with 
    # mapnum (map#2 for ign map), rpm_count, 16 unknown bytes, 
    # then rpm labels and ign map (again in column-order).
    # NOTE!! The ignition rpm values (e.g., the rpm_count) are often DIFFERENT than the fuel ones!
    
    $buff=my_read(*INFILE,4);
    ($mapnum,$rpm_count)=unpack("vv",$buff);  
    
    $h{ign_rpm_count}=$rpm_count;

    # Next is another 16 bytes that I have no idea about. 
    $buff=my_read(*INFILE,16);
   
    # Read IGN RPMs 
    $buff=my_read(*INFILE,$rpm_count*4);
    (@rpm_labels)=unpack('V*',$buff);

    @{$h{ign_rpm_labels}}=@rpm_labels;
   
    # Read IGN MAP (This read takes us to the very end of the file).
    $buff=my_read(*INFILE,$rpm_count*4*9);   
    (@arr)=unpack('i*',$buff);  # 9 columns of data in one big 'ol array.

    # Re-order IGN MAP:
    @arr2=();
    for ($rpm=0; $rpm<$rpm_count; $rpm++) {
        for ($tp=0; $tp<9; $tp++) {
            $arr2[$rpm*9 + $tp]=$arr[ $tp*$rpm_count + $rpm ];
        }
    }

    @{$h{ign_map}}=@arr2;


    # Store a PC5 version of the fuel map
    {
        my(@tp_array)=(0,2,5,10,20,40,60,80,100);
        my(@fuel_array2)=();
        my(@fuel_array)=();
        my($num,$tp_count);
        @fuel_array=@{$h{pc3_fuel_map}};
        $tp_count=9;
        for ($rpm=0; $rpm<$rpm_count; $rpm++) {
            for ($tp=0; $tp<$tp_count; $tp++) {
                push(@fuel_array2,$fuel_array[ $rpm*$tp_count + $tp ]);
                # Insert interpolated TP=15 column:
                if ($tp_array[$tp] == 10) {
                    # my $num=int(  ($fuel_array[$rpm*$tp_count+$tp] + $fuel_array[$rpm*$tp_count+$tp +1])/2 + .5);
                    my $num=($fuel_array[$rpm*$tp_count+$tp] + $fuel_array[$rpm*$tp_count+$tp +1])/2 ;
                    $num=int($num + .5 * ($num <=> 0));  # strange-looking perl rounding func that handles negatives
                    push(@fuel_array2,$num);
                }
            }
        }
        @{$h{pc5_fuel_map}}=@fuel_array2;

    }


    
    close INFILE;

    return(%h); 
    
}
###########################################################
#
# This func reads a PC5 (.pvm) file. The useful stuff will
# be stored in a hash (%h) and returned.
#
#
sub read_pvm_file
{
    my ($file,$verbose) = @_;
    my ($buff,%h,$len,$rpm_count,@rpm_labels,@arr,$rpm,$tp,$i,$cyls,$offset);
    my ($filesize);
   
    print "Opening map file: $file\n"  if ($verbose);
    if (! open(INFILE, "$file") ) {
        print "Unable to read $file ($!) - skipping it.\n";
        return;
    }
    binmode(INFILE);

    %h=();
    @arr=();

    ($filesize)=(stat($file))[7];

    print "size of $file is $filesize bytes\n" if ($verbose);

    # @arr=((0) x 100);


    # Read past 1st 64 bytes, but check the 1st 8-bit integer (should be a 2).
    $buff=my_read(*INFILE,64);
    (@arr)=unpack("C*",$buff);
    # PC5/PVM maps seem to have a value of '2' in the first byte.
    if ( $arr[0] != 2 ) {
        print "Questionable value in byte 1 ($arr[0]) - Aborting map!\n";
        close INFILE;
        return;
    }

    $cyls=$arr[20];  # 21st and 22nd byte should each be the cylinder count (1,2,3 or 4)
    if ( ($cyls < 1)||($cyls > 6) ) {
        print "Questionable cylinder count in byte 21 ($cyls) - Aborting map!\n";
        close INFILE;
        return;
    }
    print "cyls_count=$cyls\n" if ($verbose);

    $offset=$arr[48];  # 48th byte seems to be some sort of offset value

    if ( ($offset < 1)||($offset > 20) ) {
        print "Questionable offset value in byte 48 ($offset) - Aborting map!\n";
        close INFILE;
        return;
    }
    print "offset=$offset\n" if ($verbose);


    $h{map_type}="PC5";
    $h{map_file}=$file;
    $h{map_basename}=basename($file);
    $h{map_dirname}=dirname($file);
    $h{cyls_count}=$cyls;

    # $buff=my_read(*INFILE,272+$cyls*16);  
    $buff=my_read(*INFILE,192+$offset*16);  
    (@arr)=unpack("C*",$buff);

    $rpm_count=$arr[$#arr-3];  # 4th to the last byte is rpm count

    print "rpm_count=$rpm_count\n" if ($verbose);
    if ( ($rpm_count < 20) || ( $rpm_count > 80 ) ) {
        print "Questionable rpm_count ($rpm_count)- Aborting map!\n";
        close INFILE;
        return;
    }
    $h{fuel_rpm_count}=$rpm_count;

    $buff=my_read(*INFILE,2*$rpm_count);
    (@rpm_labels)=unpack("S*",$buff);

    print "RPM Labels : @rpm_labels\n" if ($verbose);

    @{$h{fuel_rpm_labels}}=@rpm_labels;

    # we've now read 64 + 192 + 16*$offset + 2*$rpm_count bytes.
    # we need to read past 448 + 16*$offset bytes to the fuel table.
    # 448 + 16*$offset - (256 + 16*$offset + 2*$rpm_count)
    # = 448 -256 - 2*$rpm_count = 192 - 2*$rpm_count more bytes to read.
    # So, we'd be in trouble if there was a bike with rpm_count > 96 (equates to a 24,250+ rpm bike)

    $len=192 - 2*$rpm_count;
    # read past the 'filler' before the fuel map.
    $buff=my_read(*INFILE,$len);  


    # Read the fuel map (10 TPs x $rpm_count long).
    $buff=my_read(*INFILE,10*$rpm_count);
    (@arr)=unpack("C*",$buff);
    print "Unadjusted fuel map : @arr\n" if ($verbose);

    # Need to subtract 100 from each cell to get actual fuel map.
    # E.g., a value of 64h (=100) means cell value=0.
    # So, max neg value is -100, and max pos value is 155.
    for ($i=0; $i<10*$rpm_count; $i++) {
        $arr[$i]-=100;
    }

    @{$h{pc5_fuel_map}}=@arr;  # unmodified pc5 map

    if ( ($filesize - (448 + 16*$offset)) < (30*$rpm_count)) {
        # Some pc5 maps are cut off after the fuel_table and only have comments/text following.
        # If so, dummy up the other values...
        my($i,@zeroes);

        @zeroes=();

        for ($i=0; $i<10*$rpm_count; $i++) {
            $zeroes[$i]=0;
        }

        @{$h{afr_table}}=@zeroes;
        @{$h{trim_table}}=@zeroes;
        @{$h{unknown_table}}=@zeroes;
        # @{$h{zeroes}}=@zeroes;
        print "No AFR,TRIM, or Unknown Table in this map\n" if ($verbose);
        $h{pc5_map_type}="Shortened PC5 Map";


    }else{

        # Read the AFR table. Each byte is AFR value multiplied by 10 (e.g., 132 => 13.2).
        $buff=my_read(*INFILE,10*$rpm_count);
        (@arr)=unpack("C*",$buff);
        print "Unadjusted AFR table : @arr\n" if ($verbose);
    
        @{$h{afr_table}}=@arr;
    
        # Read the TRIM table. Each [signed] byte is the actual trim.
        $buff=my_read(*INFILE,10*$rpm_count);
        (@arr)=unpack("c*",$buff);
        print "TRIM table : @arr\n" if ($verbose);
    
        @{$h{trim_table}}=@arr;
    
        # The next table might be the Ignition map - not sure. 
        $buff=my_read(*INFILE,10*$rpm_count);
        (@arr)=unpack("c*",$buff);
        print "Unknown table (IGN??) : @arr\n" if ($verbose);
    
        @{$h{unknown_table}}=@arr;
    }

    #
    # Store a pc3 map (with and without trim), and the pc5 map w/trim applied, plus trim stats:
    #
    {
        my($trim,$trim_sum,$trim_neg,$trim_pos)=(0,0,0,0);
        my(@fuel_array,@fuel_array2,$tp_count,$tp_count2,@tp_array,@tp_array2);
        my(@fuel_array_no_trim,@fuel_array2_no_trim);
        @fuel_array_no_trim=@{$h{pc5_fuel_map}};  # pc5 fuel map w/o trim
        @fuel_array=@{$h{pc5_fuel_map}};  # pc5 fuel map w/o trim
        $tp_count=10;
        @tp_array=(0,2,5,10,15,20,40,60,80,100);
        $tp_count2=9;
        @tp_array2=(0,2,5,10,20,40,60,80,100);
        # Apply the Autotune trim setting
        for ($i=0; $i<$tp_count*$rpm_count; $i++) {
            # Take note of how many non-zero trim cells there were.
            if(${$h{trim_table}}[$i]) {
                my ($trimval);
                $trim++;
                $trimval=${$h{trim_table}}[$i];
                $trim_sum+=$trimval;
                if($trimval > 0) {
                    $trim_pos++;
                }else{
                    $trim_neg++;
                }
                
                $fuel_array[$i] += $trimval;
            }
        }

        @{$h{pc5_fuel_map_with_trim}}=@fuel_array;  # store pc5 map w/ trim
        @{$h{fuel_map}}=@fuel_array;      # common name for scripts to work with

        # Some trim stats
        $h{trim_cell_count}=$trim; # nonzero trim count
        $h{trim_cell_pos_count}=$trim_pos;
        $h{trim_cell_neg_count}=$trim_neg;
        $h{trim_cell_sum}=$trim_sum;

        # Convert to PC3 by removing the 5th column (TP=15).
        @fuel_array2=();
        @fuel_array2_no_trim=();
        for ($rpm=0; $rpm<$rpm_count; $rpm++) {
            for ($tp=0; $tp<$tp_count; $tp++) {
                next if ($tp_array[$tp] == 15);
                push(@fuel_array2,$fuel_array[ $rpm*$tp_count + $tp ]);
                push(@fuel_array2_no_trim,$fuel_array_no_trim[ $rpm*$tp_count + $tp ]);
            }
        }

        @{$h{pc3_fuel_map_with_trim}}=@fuel_array2;  # store pc3 map w/ trim
        @{$h{pc3_fuel_map}}=@fuel_array2_no_trim;    # store pc3 map w/o trim
    }
    

    # The last 256 chars is for TEXT. Dynojet s/w terminates the text with a zero byte.
    $buff=my_read(*INFILE,256);
    (@arr)=unpack("C*",$buff);
    $text=unpack("a*",$buff);

    $len=0;
    for ($i=0; $i<256; $i++) {
        $char=$arr[$i];
        if ($arr[$i] == 0) {  # NULL byte terminates string.
            $len=$i;
            last;
        }
    }

    $text=substr($text,0,$len);
    $h{text}=$text;
    print "Text : $text\n" if ($verbose);

    close INFILE;

    return(%h); 
    
}
###########################################################
1;
